/**
 * Copyright 2008 Mathias Doenitz, http://lis.to/
 *
 * This file is part of the lis.to java desktop client. The lis.to java desktop client is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * The lis.to java desktop client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with the lis.to java desktop client.
 * If not, see http://www.gnu.org/licenses/
 */

package listo.utils.swing;

import org.apache.commons.lang.StringUtils;

import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;

public class AutoCompleter extends JPanel {

    public enum EventType {
        UserRequest,    // the user explicitly requested the autocompletion options
        FocusGained,    // the focus just changed to the underlying text component
        TextTyped       // the user is in the middle of typing text
    }

    public class Context {
        private JDialog dialog;
        private JTextComponent textComponent;
        private AutoCompleter completer;
        private JList optionsList;
        private String currentWord;
        private EventType eventType;
        private List<String> options = new ArrayList<String>();
        private int selectedIx;
        private String currentWordTrimmedLower;

        public AutoCompleter getCompleter() {
            return completer;
        }

        public JTextComponent getTextComponent() {
            return textComponent;
        }

        public JList getOptionsList() {
            return optionsList;
        }

        public void setCurrentWord(String word) {
            context.currentWord = word;
            context.currentWordTrimmedLower = word.trim().toLowerCase();
        }

        public String getCurrentWord() {
            return currentWord;
        }

        public String getCurrentWordTrimmedLower() {
            return currentWordTrimmedLower;
        }

        public EventType getEventType() {
            return eventType;
        }

        public List<String> getOptions() {
            return options;
        }

        public void select(int ix) {
            selectedIx = ix;
        }
    }

    public interface CompletionProvider {
        /**
         * Determines whether the given character is a word separator.
         *
         * @param c the character
         * @return true if a word separator
         */
        public boolean isWordSeparator(char c);

        /**
         * Collects the options for the current input context.
         *
         * @param context the autocompletion context to operate on
         */
        public void provideAutoCompletion(Context context);
    }

    private final Context context = new Context();
    private final CompletionProvider completionProvider;

    public AutoCompleter(CompletionProvider completionProvider) {
        super(new FlowLayout(FlowLayout.LEADING, 0, 0));
        this.completionProvider = completionProvider;
    }

    public void decorate(JDialog dialog, JTextComponent textComponent) {
        context.dialog = dialog;
        context.textComponent = textComponent;
        context.completer = this;
        context.optionsList = new JList() {
            public int getVisibleRowCount() {
                return Math.min(context.options.size(), 8);
            }
        };
        context.optionsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        context.optionsList.setBackground(SystemColor.info);

        setBorder(BorderFactory.createRaisedBevelBorder());

        JScrollPane scrollPane = new JScrollPane(context.optionsList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        add(scrollPane);

        JLayeredPane layeredPane = dialog.getLayeredPane();
        layeredPane.add(this, JLayeredPane.POPUP_LAYER);

        hideOptions(); // initialize the state to "not visible"

        textComponent.addKeyListener(new KeyAdapter() {
            public void keyPressed(final KeyEvent e) {
                if (e.isConsumed()) return;
                if (AutoCompleter.this.isVisible()) {
                    int ix = context.optionsList.getSelectedIndex();
                    int count = context.optionsList.getModel().getSize();
                    switch (e.getKeyCode()) {
                        case KeyEvent.VK_UP:
                        case KeyEvent.VK_PAGE_UP:
                            if (ix == -1 || ix == 0) {
                                ix = count - 1;
                                context.optionsList.setSelectedIndex(ix);
                                context.optionsList.ensureIndexIsVisible(ix);
                                e.consume();
                            } else {
                                context.optionsList.dispatchEvent(e);
                            }
                            return;

                        case KeyEvent.VK_DOWN:
                        case KeyEvent.VK_PAGE_DOWN:
                            if (ix == -1 || ix == count - 1) {
                                ix = 0;
                                context.optionsList.setSelectedIndex(ix);
                                context.optionsList.ensureIndexIsVisible(ix);
                                e.consume();
                            } else {
                                context.optionsList.dispatchEvent(e);
                            }
                            return;

                        case KeyEvent.VK_ENTER:
                            if (isSomethingSelected()) {
                                select();
                                context.textComponent.requestFocusInWindow();
                                e.consume();
                            }
                            return;

                        case KeyEvent.VK_ESCAPE:
                            hideOptions();
                            e.consume();
                            return;

                        default:
                            if (isSomethingSelected() && completionProvider.isWordSeparator(e.getKeyChar())) {
                                select();
                                context.textComponent.requestFocusInWindow();
                                return;
                            }
                    }
                }

                final boolean upOrDownPressed = e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_UP;
                if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED || upOrDownPressed || (AutoCompleter.this.isVisible() &&
                        (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_RIGHT))) {

                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            String text = context.textComponent.getText();
                            if (StringUtils.isNotEmpty(text) || upOrDownPressed) {
                                updateOptionsList(upOrDownPressed ? EventType.UserRequest : EventType.TextTyped);
                            } else {
                                hideOptions();
                            }
                        }
                    });
                }
            }
        });

        textComponent.addFocusListener(new FocusListener() {
            public void focusLost(final FocusEvent e) {
                if (e.getOppositeComponent() == context.optionsList) {
                    context.textComponent.requestFocusInWindow();
                    return;
                }
                if (isVisible()) {
                    if (isSomethingSelected()) {
                        select();
                    } else {
                        hideOptions();
                    }
                }
            }

            public void focusGained(final FocusEvent e) {
                // we add the focus listener code for the text component in "invokeLater"
                // since we want to "be last" in the chain of focus listener code running,
                // maybe some other controller code wants to run before us here
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        if (e.getOppositeComponent() != context.optionsList) {
                            updateOptionsList(EventType.FocusGained);
                        }
                    }
                });
            }
        });

        context.optionsList.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                select();
                context.textComponent.requestFocusInWindow();
            }
        });
    }

    private boolean isSomethingSelected() {
        return context.optionsList.getSelectedIndex() != -1;
    }

    private void updateOptionsList(EventType eventType) {
        context.eventType = eventType;
        context.options.clear();
        context.selectedIx = -1;
        String selText = context.textComponent.getSelectedText();
        context.setCurrentWord(StringUtils.isEmpty(selText) ? getCurrentWord() : selText);

        completionProvider.provideAutoCompletion(context);

        if (context.options.isEmpty()) {
            hideOptions();
        } else {
            showOptions();
        }
    }

    private void hideOptions() {
        setVisible(false);
    }

    private void showOptions() {
        int index = context.textComponent.getCaretPosition() - getCurrentWordLength();
        try {
            // update list
            context.optionsList.setListData(context.options.toArray());
            context.optionsList.revalidate();
            context.optionsList.setSelectedIndex(context.selectedIx);
            if (context.selectedIx != -1) {
                context.optionsList.ensureIndexIsVisible(context.selectedIx);
            }

            // update size
            validate();
            Dimension dim = getPreferredSize();
            setSize(dim);

            // position popup
            Rectangle offset = context.textComponent.getUI().modelToView(context.textComponent, index);
            Component component = context.textComponent;
            while (!(component instanceof JLayeredPane)) {
                Point loc = component.getLocation();
                offset.x += loc.x;
                offset.y += loc.y;
                component = component.getParent();
            }
            Point loc = new Point(offset.x - 3, offset.y + offset.height + 1);

            // if the window would be (partially) outside of the dialog, position it above the cursor
            if (loc.y + dim.height > context.dialog.getHeight()) loc.y -= dim.height + offset.height + 3;
            if (loc.x + dim.width > context.dialog.getWidth()) loc.x = context.dialog.getWidth() - dim.width - 5;
            setLocation(loc);

            //  display
            setVisible(true);
        }
        catch (BadLocationException e) {
            throw new IllegalStateException(e);
        }
    }

    private void select() {
        try {
            Document document = context.textComponent.getDocument();
            String text = (String) context.optionsList.getSelectedValue();
            int wordStart;
            int eraseLen;

            if (StringUtils.isEmpty(context.textComponent.getSelectedText())) {
                int currentWordLength = getCurrentWordLength();
                wordStart = context.textComponent.getCaretPosition() - currentWordLength;
                eraseLen = currentWordLength;
                while (text.startsWith(document.getText(wordStart, eraseLen + 1))) eraseLen++;
            } else {
                wordStart = context.textComponent.getSelectionStart();
                eraseLen = context.textComponent.getSelectionEnd() - wordStart;
            }
            document.remove(wordStart, eraseLen);
            document.insertString(wordStart, text, null);
            context.textComponent.setCaretPosition(wordStart + text.length());
        }
        catch (BadLocationException e) {
            throw new IllegalStateException(e);
        }

        hideOptions();
    }

    private int getCurrentWordLength() {
        int pos = context.textComponent.getCaretPosition();
        if (pos == 0) return 0;
        String text = context.textComponent.getText();
        int index = pos - 1;
        while (index >= 0) {
            if (completionProvider.isWordSeparator(text.charAt(index))) return pos - index - 1;
            index--;
        }
        return pos;
    }

    private String getCurrentWord() {
        int pos = context.textComponent.getCaretPosition();
        int len = getCurrentWordLength();
        return len > 0 ? context.textComponent.getText().substring(pos - len, pos) : "";
    }

}